<?php
/**
 * Created by 784855684@qq.com.
 * Link: https://github.com/lichtung/wshore
 * User: linzh
 * Date: 6/25/17
 * Time: 5:30 PM
 */
declare(strict_types=1);


namespace wshore\core;


use wshore\Component;
use Redis as _Redis;
use wshore\throws\db\RedisConnectException;
use wshore\throws\ExtensionRequiredException;

/**
 * Class Redis
 *
 * @method  Redis getInstance($index = 0, array $params = []) static
 * @package wshore\core
 */
class Redis extends Component
{
    /**
     * @var _Redis
     */
    private $handler = null;

    public function __construct($params = null)
    {
        parent::__construct([
            'host' => '127.0.0.1',
            'password' => NULL,
            'port' => 6379,
            'timeout' => 0.0,
            'database' => 0
        ]);
        $this->handler();
    }

    public function __destruct()
    {
        if ($this->handler) {
            $this->handler->close();
        }
    }

    /**
     * 获取redis句柄
     * @return _Redis
     * @throws ExtensionRequiredException
     * @throws RedisConnectException
     */
    public function handler(): _Redis
    {
        if (!$this->handler) {
            if (!extension_loaded('redis')) {
                throw new ExtensionRequiredException('redis');
            }
            $this->handler = new _Redis();
            try {
                $host = $this->_config['host'];
                $port = $this->_config['port'];
                $timeout = $this->_config['timeout'];
                $password = $this->_config['password'];
                $database = $this->_config['database'];

                if (!$this->handler->connect($host, ($host[0] === '/' ? 0 : $port), $timeout))
                    throw new RedisConnectException(var_export([$host, $port, $timeout, 'connect failed!'], true));
                if ($password and !$this->handler->auth($password))
                    throw new RedisConnectException(var_export([$host, $port, $timeout, $password, 'authentication failed'], true));
                if ($database and !$this->handler->select($database))
                    throw new RedisConnectException(var_export([$host, $port, $timeout, $password, $database, 'database select failed'], true));

            } catch (\RedisException $e) {
                throw new RedisConnectException(var_export([$host, $port, $timeout, $password, $database, $e->getMessage()], true));
            }
        }
        return $this->handler;
    }

    /**
     * @param string $id
     * @param mixed $data
     * @param int $ttl 过期时间
     * @return bool
     */
    public function set(string $id, $data, int $ttl = WS_ONE_HOUR): bool
    {

        switch ($data_type = gettype($data)) {
            case 'array':
            case 'object':
                $data = serialize($data);
                break;
            case 'boolean':
            case 'integer':
            case 'double': // Yes, 'double' is returned and NOT 'float'
            case 'string':
            case 'NULL':
                break;
            case 'resource':
            default:
                return FALSE;
        }

        $this->delete($id);
        if (!$this->handler->hMSet($id, array('__ci_type' => $data_type, '__ci_value' => $data))) {
            return FALSE;
        } elseif ($ttl) {
            $this->handler->expireAt($id, time() + $ttl);
        }
        return TRUE;
    }

    /**
     * @param string $key
     * @param mixed $replace
     * @return mixed
     */
    public function get(string $key, $replace = null)
    {
        $data = $this->handler->hMGet($key, array('__ci_type', '__ci_value'));
        if (!isset($data['__ci_type'], $data['__ci_value']) OR $data['__ci_value'] === FALSE) {
            return $replace;
        }
        switch ($data['__ci_type']) {
            case 'array':
            case 'object':
                return unserialize($data['__ci_value']);
            case 'boolean':
            case 'integer':
            case 'double': // Yes, 'double' is returned and NOT 'float'
            case 'string':
            case 'NULL':
                return settype($data['__ci_value'], $data['__ci_type'])
                    ? $data['__ci_value']
                    : $replace;
            case 'resource':
            default:
                return $replace;
        }
    }

    /**
     * 删除指定的键
     * @param string[] ...$names
     * @return bool
     */
    public function delete(string ...$names): bool
    {
        return call_user_func_array([$this->handler, 'delete'], $names) > 0;
    }

    /**
     * @param string $key
     * @return int 返回新的值
     */
    public function increase(string $key): int
    {
        $res = $this->handler()->incr($key);
        if (false === $res) {
            dumpout($key, $this->handler()->getLastError());
        }
        return $res;
    }

    /**
     * @param string $key
     * @return int 返回新的值
     */
    public function decrease(string $key): int
    {
        return $this->handler()->decr($key);
    }

    /**
     * 删除全部
     * @return bool
     */
    public function deleteAll(): bool
    {
        return $this->handler->flushDB();
    }

    /**
     * 获取redis服务器
     * <pre>
     *  $redis->connect('/tmp/redis.sock');      // unix domain socket.
     * </pre>
     * @param  string $host can be a host, or the path to a unix domain socket
     * @param  int $port optional
     * @param  float $timeout value in seconds (optional, default is 0.0 meaning unlimited)
     * @param string $password Authenticate the connection using a password. @Warning: The password is sent in plain-text over the network.
     * @param int $database index of database.
     * @return null|_Redis
     */


//-------------------------------- 队列操作 ------------------------------------------//

    /**
     * 由列表头部添加字符串值。
     *
     * Adds the string values to the head (left) of the list. Creates the list if the key didn't exist.
     * If the key exists and is not a list, FALSE is returned.
     *
     * @param string $key
     * @param string $value
     * @param bool $only_exist
     * @return bool
     */
    public function prepend(string $key, string $value, bool $only_exist = false): bool
    {
        $key = '__list__' . $key;
        $res = call_user_func([$this->handler, $only_exist ? 'lPushx' : 'lPush'], $key, $value);
        return $res !== false;
    }

    /**
     * 由列表尾部添加字符串值
     * @param string $key
     * @param string $value
     * @param bool $only_exist
     * @return bool
     */
    public function append(string $key, string $value, bool $only_exist = false): bool
    {
        $key = '__list__' . $key;
        $res = call_user_func([$this->handler, $only_exist ? 'rPushx' : 'rPush'], $key, $value);
        return $res !== false;
    }

    /**
     * 返回并删除首个元素
     * get and removes the first element of the list.
     * @param string $key
     * @param mixed $replace
     * @return array|string|null
     */
    public function shift(string $key, $replace = null)
    {
        $key = '__list__' . $key;
        $element = $this->handler->lPop($key);
        return false === $element ? $replace : $element;
    }

    /**
     * 返回并删除最后一个元素
     * Returns and removes the last element of the list.
     * @param string $key
     * @param mixed $replace
     * @return array|string|null
     */
    public function pop(string $key, $replace = null)
    {
        $key = '__list__' . $key;
        $element = $this->handler->rPop($key);
        return false === $element ? $replace : $element;
    }

    /**
     * 返回序列长度
     * @param string $key
     * @return int
     */
    public function length(string $key): int
    {
        $len = $this->handler->lLen($key);
        return false === $len ? 0 : $len;
    }

    /**
     * 获取队列中的全部元素
     * @param string $key
     * @return array
     */
    public function all(string $key): array
    {
        return $this->handler->lRange($key, 0, -1);//第一个到最后一个
    }

    /**
     * 获取指定位置的元素
     * @param string $key
     * @param int $index
     * @param null $replace
     * @return mixed
     */
    public function index(string $key, int $index, $replace = null)
    {
        $res = $this->handler->lGet($key, $index);
        return false === $res ? $replace : $res;
    }
//---------------------------- 通用操作 ------------------------------------------//

    /**
     * 验证指定的键是否存在
     * @param string $name
     * @return bool
     */
    public function has(string $name): bool
    {
        return (bool)$this->handler->exists($name);
    }

    /**
     * 获取缓存
     * @param string $name
     * @param \Closure|mixed $replace 如果是一个闭包，则值不存在时获取并设置缓存
     * @param int $expire Closure返回值的缓存期
     * @return mixed
     */
    public function getSet(string $name, $replace = '', int $expire = WS_ONE_HOUR)
    {
        if ($replace instanceof \Closure) {
            $value = $this->get($name, '');
            if ('' === $value) {
                self::set($name, $value = $replace(), $expire);
            }
        } else {
            $value = $this->get($name, $replace);
        }
        return $value;
    }

    /**
     * 获取上一次发生的错误
     * @param string $error
     * @return string
     */
    public function getError(string $error = ''): string
    {
        return $this->handler()->getLastError()??$error;
    }

}